
在營養教育的時候常會介紹整個餐點製作的過程,稱為「農場到餐桌」,我們的 code 就像食材,確定了網站的基礎結構和用戶體驗;而 Rendering 則像烹飪技巧,決定了頁面的呈現方式和效能。
在 13 版更新後使用以 React Server Component 與 Client Component 切分作為宣傳招牌,以下來簡單的了解 Server Component 與 Client Component 在專案中的使用模式,以及所謂的靜態渲染和動態渲染與 SSR 那些渲染有什麼關係?


在檔案第一行,所有 import 之前標註
"use client"
'use client';
import useRouter from 'next/navigation'
export default function ClientComponent() {
const router = useRouter()
return (
<div>
<button onClick={() => router.push('/login')}>Login</button>
</div>
);
}
在 app 目錄中,所有 component 預設都是 Server Component,而 Server 及 Client Component 可以共存,讓開發者選擇這個 component 想要渲染的環境。
在 Server Component 中的 component 可以保證只在 server side render
在 Client Component 中的 component 雖然主要在 client render,但 Next.js 中可以在 server side 預渲染,再於 client hydrate,主要使用useSWR或直接將 server componentfetch到的 data 傳給 client component
在程式碼中我們可以用 “use client” 來分辨是否為 Client Component,那麼在網頁上我們如何分辨呢?最重要的方法也是 Server Component 最大的優點:Server Component 不會包含在 JavaScript bundle 中,因此也減少了 bundle 的大小,減少前端網頁的載入量,從而提升效能。

Server Component 中可以引入 Client Component
// app/layout.tsx
import ServerComponent from './ServerComponent'
import ClientComponent from './ClientComponent'
// Layout 預設就是 Server Component
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<ClientComponent />
<ServerComponent />
</nav>
<main>{children}</main>
</>
)
}
從 Server Component 傳遞序列化的 props 給 Client Component
JavaScript 中,指的是將資料結構轉換為 JSON 格式的 string (JSON.stringify),以下幾個可能不小心就用到的非序列化格式:
function() { return 1; } 不能被序列化為 JSON。undefined:JSON不支持 undefined。RegExp, Map, Set, Date,這些在轉為 JSON 時不會保留其原始結構和方法。Error物件:雖然能夠序列化,但它的某些屬性(如 stack)可能不會完整地被序列化。Client Component 中不可以直接引用 Server Component
// ❌ 會出現錯誤
'use client';
import useRouter from 'next/navigation'
import ServerComponent from './ServerComponent'
export default function Counter() {
const router = useRouter()
return (
<div>
<button onClick={() => router.push('/login')}>Login</button>
<ServerComponent/>
</div>
);
}
Server Component 可以作為 Client Component 的 children
// app/ClientComponent.js
'use client';
import useRouter from 'next/navigation'
export default function ClientComponent({children}) {
const router = useRouter()
return (
<div>
<button onClick={() => router.push('/login')}>Login</button>
{children}
</div>
);
}
// app/page.js
import ClientComponent from "./ClientComponent";
import ServerComponent from "./ServerComponent";
// Pages 預設是 Server Component
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
);
}
第三方套件若有預設 client-only 的功能,可先封裝為 Client Component 後再引進 Server
'use client'
import { SessionProvider } from "next-auth/react"
export default SessionProvider
Provider 的實作方法:將所有 provider 封裝為一個 Provider component,再於 Root Layout 調用
// providers/Providers.tsx
'use client'
import React, { FC } from 'react'
import { ThemeProvider } from 'next-themes'
import { type ThemeProviderProps } from 'next-themes/dist/types'
import ToasterProvider from './ToastProvider'
const Providers: FC<ThemeProviderProps> = ({ children, ...props }) => {
return (
<ThemeProvider {...props}>
<ToasterProvider />
{children}
</ThemeProvider>
)
}
export default Providers
// app/layout.ts
export default async function RootLayout({ children, auth }: RootLayoutProps) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<Providers attribute="class" enableSystem>
{children}
</Providers>
</body>
</html>
)
}
基本上有用戶與網頁的交互作用就會視為 Client Component
| 情境 | Server Component | Client Component |
|---|---|---|
| 獲取數據 | ✅ | ❌ |
| 直接訪問後端資源 | ✅ | ❌ |
| 將敏感信息 (access tokens, API keys, etc) 保存在 server 上 | ✅ | ❌ |
| 將大型依賴項保留在服務器上 / 減少 client 端 JavaScript | ✅ | ❌ |
| 使用互動性及監聽事件(onClick(), onChange(), etc) | ❌ | ✅ |
| 使用 State 和生命週期 Effects (useState(), useReducer(), useEffect(), etc) | ❌ | ✅ |
| 使用 browser-only APIs ( localStorage, XMLHttpRequest, etc) | ❌ | ✅ |
| 使用依賴於 state、effects 或 browser-only APIs 的自定義 hooks | ❌ | ✅ |
| 使用 React Class components | ❌ | ✅ |
在 13 版本中可以發現各處都看不見 SSR / SSG / ISR 這些渲染的介紹,取而代之的是 Static rendering 與 dynamic rendering ,他們之間有什麼關聯,各自又是什麼?下方帶各位一探究竟。
當資料請求有被暫存且未 Dynamic Functions 才會實現靜態渲染,Nextjs 預設為靜態渲染
Server 及 Client Component 會在 build time 做預渲染,預渲染的結果會被暫存並在後續的請求中重用,而暫存的結果可以被重新生效(revalidated)
有使用 Dynamic Functions 或 Dynamic Fetches 時實現動態渲染
Server 及 Client Component 會在 request time 被渲染,渲染的內容不會被暫存
在 Server Component 中使用 cookies() 或 headers()
在 Client Component 中使用 useSearchParams()
官方建議在靜態渲染的 route 中,如果有子組件使用了
useSearchParams()會影響其他相近的所有 client component 渲染模式,建議以<Suspense/>包覆,可以隔絕該 component,使其他 components 依舊為靜態渲染
page.js 頁面有取得 searchParams - Server Component 及 Client Component 皆可以取得
export default function Page({
params,
searchParams,
}: {
params: { slug: string }
searchParams: { [key: string]: string | string[] | undefined }
}
) {
return (
<div>
// 內容
</div>
);
}
執行 fetch() 請求時,請求的 Data 沒有 cache 的行為會被視為 Dynamic Fetches ,在 13 版不再使用 get…Props() 的方式進行資料的請求,取而代之的是 fetch() api 並且在所有的 component 都可以調用,不再只限定於 Page component,下方條列出的情況 data 不會被 cache:
fetch('https://...', { cache: 'no-store' })
單獨調用 fetch(https://..., { next: { revalidate: 0 } }) 請求時
當 fetch 請求位於使用 POST 方法的 Route handler 中時:因為 Route handler 不在 React component tree 之中
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const data = await fetch('https://...')
const res = await request.json()
return NextResponse.json({ res })
}
當使用 headers 或 cookies 之後進行 fetch 請求時
import { headers } from 'next/headers'
async function getUser() {
const headersInstance = headers()
const authorization = headersInstance.get('authorization')
const res = await fetch('...', {
headers: { authorization },
})
return res.json()
}
當使用 const dynamic = 'force-dynamic' 時
export const fetchCache = 'default-no-store’
當 fetch 請求使用 Authorization 或 Cookie headers,且在組件樹中它上面有一個未被緩存的請求時。
如果以 page route 的概念來看靜態渲染及動態渲染,就會是下面三種組合
getStaticProps)getStaticProps with revalidate props)getServerSideProps)阿怎麼又多了兩種 fetch ?
別擔心官網有告訴你:
// `force-cache` 是預設可以省略
const staticData = await fetch(`https://...`, { cache: 'force-cache' })
// 'no-store' 是不做緩存
const dynamicData = await fetch(`https://...`, { cache: 'no-store' })
// 設置 revalidate 時間
const revalidatedData = await fetch(`https://...`, {
next: { revalidate: 10 },
})
對於新版的 Nextjs 感覺需要拋去許多固有的用法轉而新的概念,對新的使用者也許學習曲度不會這麼高,不過相信如果是公司舊有的專案,許多也尚未更新到最新的版本,雖然可以兩種形式並存但轉移也需要時間成本,必較兩種不同的模式也是很有趣的,就像多學了很多新知識一樣。而 Vercel 很貼心的做了一些新功能範例供開發者參考,有空可以玩玩並看一下程式碼的實踐方法:
https://app-router.vercel.app/
Static and dynamic rendering in Next 13
https://dev.to/peterlidee/static-and-dynamic-rendering-in-next-13-52gb
【react】初探server component
https://juejin.cn/post/6918602124804915208#heading-18
Understanding React Server Components
https://vercel.com/blog/understanding-react-server-components
React Server vs Client components in Next.js 13
https://kulkarniankita.com/react/react-server-client-components
